package com.eischet.lookup.web;

import org.apache.log4j.Logger;
import org.apache.lucene.index.Term;
import org.apache.lucene.search.BooleanClause;
import org.apache.lucene.search.BooleanQuery;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.TermQuery;
import org.springframework.beans.factory.annotation.Required;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.simple.SimpleJdbcTemplate;

import javax.servlet.http.HttpSession;
import javax.sql.DataSource;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Collections;
import java.util.Set;

import static com.eischet.lookup.Coercere.toSetOfNumbers;
import static com.eischet.lookup.Functional.*;

/**
 * Provides Remedy access rights checking by wrapping Lucene queries in Filters that
 * filter out all documents for which the user does not have the required rights.
 */
public class RemedyAccessController implements AccessController {

    private static final Logger logger = Logger.getLogger(RemedyAccessController.class);
    private final static String SESSION_KEY_GROUPS = "AccessController.groups";
    public final static long GROUP_UNRESTRICTED = 1000000000L;

    private SimpleJdbcTemplate template = null;
    private boolean enabled = true;

    /**
     * @param dataSource a data source (from which a JDBC template will be created)
     */
    @Required
    public void setDataSource(DataSource dataSource) {
        this.template = new SimpleJdbcTemplate(dataSource);
    }

    /**
     * @param enabled true to enable rights checking, false to disable it
     */
    @Required
    public void setEnabled(boolean enabled) {
        if (logger.isInfoEnabled()) {
            logger.info(String.format("setEnabled(%s) -> turning %s security checks for searches", enabled, enabled ? "ON" : "OFF"));
        }
        this.enabled = enabled;
    }

    /**
     * retrieve ARS groups of which the user is a member
     * @param loginName     the user's login name for ARS
     * @return              a set of group IDs (longs)
     */
    @Override
    public Set<Long> getGroupsForUser(String loginName) {
        if (logger.isInfoEnabled()) {
            logger.info(String.format("getGroupsForUser(loginName=%s)", loginName));
        }
        if (!enabled) {
            if (logger.isInfoEnabled()) {
                logger.info("access control is not enabled, returning empty group list");
            }
            return Collections.emptySet();
        }
        Set<Long> groups = toSetOfNumbers(template.query("SELECT group_list FROM user_x WHERE login_name = ?", getStringList(), loginName));
        if (logger.isInfoEnabled()) {
            logger.info(String.format("number of user access control groups: %d", groups.size()));
        }
        if (logger.isDebugEnabled()) {
            logger.debug("access control groups: " + join(";", toStrings(ordered(groups))));
        }
        return groups;
    }

    /**
     * @return a JDBC row mapper that returns a list of Strings
     */
    public static RowMapper<String> getStringList() {
        return new RowMapper<String>() {
            public String mapRow(ResultSet rs, int rowNum) throws SQLException {
                return rs.getString(1);
            }
        };
    }

    /**
     * Rewrite the Lucene query to take into account the current user's access rights.
     *
     * @param loginName         the user's login name
     * @param session           the user's web session, used for caching the user's rights
     * @param originalQuery     the original unwrapped query
     * @return                  if enabled, a wrapped query, or else the original query untouched
     */
    @SuppressWarnings("unchecked")
	public Query rewriteQuery(String loginName, HttpSession session, Query originalQuery) {
        if (enabled) {
            Set<Long> groups = (Set<Long>) session.getAttribute(SESSION_KEY_GROUPS);
            if (groups == null) {
                if (logger.isInfoEnabled()) {
                    logger.info(String.format("loading groups for user %s from database", loginName));
                }
                groups = getGroupsForUser(loginName);
                session.setAttribute(SESSION_KEY_GROUPS, groups);
                if (logger.isDebugEnabled()) {
                    logger.debug(String.format("groups for user %s: %s", loginName, groups));
                }
            }
            if (groups.contains(GROUP_UNRESTRICTED)) {
                if (logger.isInfoEnabled()) {
                    logger.info("user " + loginName + " is unrestricted; not wrapping query");
                }
                return originalQuery;
            }
            BooleanQuery allowedGroups = new BooleanQuery();
            // virtual group "0" is for all unrestricted records
            Term groupTerm = new Term("group", "0");
            allowedGroups.add(new TermQuery(groupTerm), BooleanClause.Occur.SHOULD);
            // this special group means "unrestricted access" in remedy, but it's not always present (why? Only present on user?)
            allowedGroups.add(new TermQuery(groupTerm.createTerm(String.valueOf(GROUP_UNRESTRICTED))), BooleanClause.Occur.SHOULD);
            for (long group: groups) {
                allowedGroups.add(new TermQuery(groupTerm.createTerm(String.valueOf(group))), BooleanClause.Occur.SHOULD);
            }
            BooleanQuery wrapper = new BooleanQuery();
            wrapper.add(originalQuery, BooleanClause.Occur.MUST);
            wrapper.add(allowedGroups, BooleanClause.Occur.MUST);
            logger.info("added " + (groups.size() + 1) + " allowed groups to query!");
            return wrapper;
        } else {
            return originalQuery;
        }
    }

    /**
     * @return a String representation of this object
     */
    @Override
    public String toString() {
        return String.format("AccessController(enabled=%s)", enabled);
    }
}
